/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Xml;
using System.Xml.Linq;
using Bio;
using Bio.IO;
using Bio.Web;
using Bio.Web.Blast;
using BlastExplorer.SilverMapDataProducer;
using NCBI_Eutils.Eutils;
using QUT.Bio.SilverMap;
using QUT.Bio.SilverMap.ObjectModel;
using QUT.Bio.Util;
using DataProducer = BlastExplorer.SilverMapDataProducer.DataProvider;
using Sequence = BlastExplorer.SilverMapDataProducer.Sequence;
using WinForms = System.Windows.Forms;

namespace BlastExplorer {

	/// <summary>
	/// Interaction logic for Window1.xaml
	/// </summary>

	public partial class Window1: Window {
		DataProducer dataProducer;
		bool useBrowserProxy;
		double eValueCutoff = Settings.DefaultEValue;
		double cddEValueCutoff = Settings.DefaultCddEValue;
		int hitListSize = Settings.DefaultHitListSize;
		Taxon [] selectedTaxa;
		bool isNewBlastRequest;
		bool weCausedThisUpdate;

		#region Constructor
		/// <summary>
		/// Initialise the map and load taxonomy database.
		/// </summary>

		public Window1 () {
			InitializeComponent();

			map.Initialize( overlay, XrefCallback, NavigationCallback, new SilverMapAdapter(
				map, overlay, 0.1, new string[] { "Name", "DistanceFromReferenceSequence" }
			) );

			map.VerticalAlignment = VerticalAlignment.Stretch;
			map.View.GraphView.Canvas.MaximumScale = 12;
			map.Model.DataProducer = dataProducer = new DataProducer( statusBar );
			map.Model.ReferenceSequenceUpdated += ReferenceSequenceUpdated;

			TaxonSelector.LoadTaxa();
		}
		#endregion

		#region Method: XrefCallback
		/// <summary>
		/// A callback that respondes to user clicking the "show details" button inside the 
		/// sequence property panel.
		/// <para>In this case, we pull down protein via an Entrez query,
		/// populate a panel, and poke it into the GeneDetailsPanel in SilverMap.
		/// </para>
		/// </summary>
		/// <param name="sequencePropertyPanel">The sequqnce property panel from which the request originates.</param>
		/// <param name="sequence">The selected sequence.</param>
		/// <param name="selectedXrefs">The selected Xref objects within the sequence.</param>

		private void XrefCallback (
			SequencePropertyPanel sequencePropertyPanel,
			ILinearDomain sequence,
			IEnumerable<QUT.Bio.Util.Xref> selectedXrefs
		) {
			// TODO: replace with something either better encapsulated, or less explicitly connected to the implementation of SilverMap.
			var p = new EntrezProteinPanel();

			Action display = delegate() {
				if ( p.IsOk ) {
					map.PageLayout.GeneDetailsPanel.Children.Clear();
					map.PageLayout.GeneDetailsPanel.Children.Add( p );
					map.PageLayout.Show( map.PageLayout.GeneDetailsPanel );
				}
			};

			p.Initialise( selectedXrefs, statusBar, display );
			p.Run();
		}
		#endregion

		#region Method: NavigationCallback
		/// <summary>
		/// Callback that is invoked by SilverMap when it pans the view to centre on a sequence.
		/// <para>In this case we hide the gene details panel because it is no longer valid.</para>
		/// </summary>
		/// <param name="ignored"></param>

		private void NavigationCallback ( ILinearDomain ignored ) {
			map.PageLayout.Hide( map.PageLayout.GeneDetailsPanel );
		}
		#endregion

		#region Method: ReferenceSequenceUpdated
		/// <summary>
		/// Callback that is invoked when the reference sequence in SilverMap changes.
		/// <para>In this case, we perform a secondary blast to complete the map.</para>
		/// </summary>
		/// <seealso cref="BlastReferenceSequence"/>

		private void ReferenceSequenceUpdated () {
			if ( !isNewBlastRequest && ! weCausedThisUpdate ) {
				BlastReferenceSequence( null );
			}
		}
		#endregion

		#region Method: LoadXml
		/// <summary>
		/// Callback that is invoked when the user selects the "load xml" menu item.
		/// <para>Displays a file open dialog and replaces the contents of the silvermap 
		/// sequence collection with the contents of the selected file.</para>
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="eventArgs"></param>
		/// <seealso cref="Load( StreamReader reader )"/>

		private void LoadXml ( object sender, RoutedEventArgs eventArgs ) {
			WinForms.OpenFileDialog dialog = new WinForms.OpenFileDialog();
			dialog.Title = "Load BLAST collection";
			dialog.Filter = "Xml documents (*.xml)|*.xml|All files (*.*)|*.*";
			WinForms.DialogResult result = dialog.ShowDialog();

			if ( result == WinForms.DialogResult.OK ) {
				try {
					using ( StreamReader reader = new StreamReader( dialog.OpenFile() ) ) {
						Load( reader );
					}
				}
				catch ( Exception e ) {
					Alert.Show( e.ToString() );
				}
			}
		}
		#endregion

		#region Method: SaveXml
		/// <summary>
		/// Callback that is invoked when the user selects the "save xml" menu item.
		/// <para>Displays a file save dialog and replaces the contents of the selected 
		/// file with the contents of the silvermap sequence collection.</para>
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="eventArgs"></param>
		/// <seealso cref="Save( StreamWriter reader )"/>

		private void SaveXml ( object sender, RoutedEventArgs eventArgs ) {
			WinForms.SaveFileDialog dialog = new WinForms.SaveFileDialog();
			dialog.Title = "Save BLAST collection";
			dialog.Filter = "Xml documents (*.xml)|*.xml|All files (*.*)|*.*";
			WinForms.DialogResult result = dialog.ShowDialog();

			if ( result == WinForms.DialogResult.OK ) {
				try {
					using ( StreamWriter writer = new StreamWriter( dialog.OpenFile() ) ) {
						Save( writer );
					}
				}
				catch ( Exception e ) {
					Alert.Show( e.ToString() );
				}
			}
		}
		#endregion

		#region Method: Load ( StreamReader reader )
		/// <summary>
		/// Populate this model with the contents of an XML document as saved with Save.
		/// [http://www.mquter.qut.edu.au/SilverMap/DocFormat.html]
		/// </summary>
		/// <param name="reader">A reader attached to an input stream containing content for the model.</param>
		/// <seealso cref="Save ( StreamWriter writer )"/>

		public void Load ( StreamReader reader ) {
			try {
				XDocument xdoc = XDocument.Load( reader );
				XElement root = xdoc.Root;
				dataProducer.Xml = root.Name == "Collection" ? root : root.Element( "Collection" );
				SelectionXml = root.Element( "Selection" );
			}
			catch ( Exception e ) {
				Status.WriteLine( "Exception caught: {0}", e );
			}
		}
		#endregion

		#region Method: Save ( StreamWriter writer )
		/// <summary>
		/// Saves the contents of the model as an xml document which is then emitted to the provided StreamWriter.
		/// </summary>
		/// <param name="writer"></param>

		public void Save ( StreamWriter writer ) {
			dataProducer.model.InitialFocus = map.Model.ReferenceSequence;
			XDocument xdoc = new XDocument( new XElement( "BlastExplorer",
				dataProducer.Xml,
				SelectionXml
			) );
			xdoc.Save( writer );
		}
		#endregion

		#region Property: SelectionXml
		/// <summary>
		/// <para>Accessor: get an Xml element containing a list of selected sequence Keys.</para>
		/// <para>Mutator: Updates the selected sequence list in SilverMap to reflect the contents of the supplied xml element.</para>
		/// </summary>

		public XElement SelectionXml {
			get {
				XElement element = new XElement( "Selection" );

				foreach ( ILinearDomain x in map.Model.SelectedSequences ) {
					element.Add( new XElement( "Sequence",
						new XElement( "Source", x.Source ),
						new XElement( "Id", x.Id )
					) );
				}

				return element;
			}
			set {
				if ( value != null && value.Name == "Selection" ) {
					List<ILinearDomain> selectedSequences = new List<ILinearDomain>();

					foreach ( XElement itemElement in value.Nodes() ) {
						XAttribute sourceAttribute = itemElement.Attribute( "Source" );
						XAttribute idAttribute = itemElement.Attribute( "Id" );
						SequenceSource source = default( SequenceSource );
						string id = "";

						if ( idAttribute != null ) {
							try {
								source = (SequenceSource) Enum.Parse( typeof( SequenceSource ), sourceAttribute.Value );
								id = idAttribute.Value;
							}
							catch {
							}
						}
						else {
							source = SequenceSource.Genbank;
							id = itemElement.Value;
						}

						try {
							selectedSequences.Add( dataProducer.model.sequences[new SequenceKey( source, id )] );
						}
						catch {
						}
					}

					map.Model.SelectedSequences = selectedSequences;
				}
			}
		}
		#endregion

		#region Method: Blast
		/// <summary>
		/// Executes a Primary Blast, seeding a new collection.
		/// <para>This replaces the data collection held by SilverMap.</para>
		/// </summary>
		/// <param name="giOrAccessions">A list of GI numbers that will form the </param>
		/// <param name="taxa">A list of taxon objects that will be included in the search.</param>
		/// <param name="querySource">The database from which the query sequence is derived.</param>

		private void Blast (
			string[] giOrAccessions,
			Taxon[] taxa,
			SequenceSource querySource
		) {
			// TODO: why are some parameters held in object state but others passed via parameters?

			isNewBlastRequest = true;

			dataProducer.Clear();

			dataProducer.DoBlast(
				giOrAccessions.Join( "\n" ),
				"blastp",
				"nr",
				eValueCutoff,
				hitListSize,
				taxa,
				Dispatcher,
				useBrowserProxy,
				delegate() {
					isNewBlastRequest = false;
				},
				null,
				querySource
			);

            // Check if accession or GIs or sequence only do EntrezQuery if not sequence
            if (Regex.IsMatch(giOrAccessions[0], "^[a-zA-Z0-9._]+$")) {
                EUtil eUtil = new EUtil();
                eUtil.EntrezQuery( EUtil.Database.protein, giOrAccessions, DoRpsBlast, EntrezQueryFailed );
            }
		}
		#endregion

		#region Method: EntrezQueryFailed
		/// <summary>
		///	Callback that displays a basic message in the status bar when an entrez query fails. 
		/// </summary>

		private void EntrezQueryFailed () {
			statusBar.WriteLine( "Error doing Entrez query." );
		}
		#endregion

		#region Execute RPS Blast on bio.qut.edu.au server.
		Guid requestId = Guid.Empty;

		private void DoRpsBlast ( Dictionary<string, GBSeqType> genbankRecords ) {
			StringWriter fasta = new StringWriter();

			foreach ( var x in genbankRecords ) {
				fasta.WriteLine( ">gi|{0} {1}", x.Key, x.Value.GBSeq_definition );
				fasta.WriteLine( x.Value.GBSeq_sequence.ToUpper() );
			}

			DoRpsBlast( fasta.ToString() );
		}

		private void DoRpsBlast ( params Sequence[] sequences ) {
			StringWriter fasta = new StringWriter();

			foreach ( Sequence seq in sequences ) {
				fasta.WriteLine( ">gi|{0} {1}", seq.Id, seq.Name );
				fasta.WriteLine( seq.SequenceData.ToUpper() );
			}

			DoRpsBlast( fasta.ToString() );
		}

		private void DoRpsBlast ( string fasta ) {
			if ( !requestId.Equals( Guid.Empty ) ) {
				return;
			}
			RpsBlast.WebServiceSoapClient rpsBlast = new RpsBlast.WebServiceSoapClient();

			rpsBlast.RPSBLASTOnFileCompleted += delegate( object sender, RpsBlast.RPSBLASTOnFileCompletedEventArgs args ) {
				( new Thread( delegate() {
					RpsBlastCounter = 10;
					PollRpsBlast( delegate() {
						requestId = Guid.Empty;
					} );
				} ) ).Start();
			};

			requestId = Guid.NewGuid();

			rpsBlast.RPSBLASTOnFileAsync(
				requestId.ToString(),
				fasta,
				"CDD",
				string.Format( "-e {0}", cddEValueCutoff )
			);
		}

		int RpsBlastCounter;

		private void PollRpsBlast ( Action cleanup ) {
			// Debug.WriteLine( String.Format( "PollRpsBlast: rpsBlastCounter = {0}", RpsBlastCounter ) );
			Thread.Sleep( 1000 );

			RpsBlast.WebServiceSoapClient rpsBlast = new RpsBlast.WebServiceSoapClient();

			rpsBlast.GetRPSBLASTResultCompleted += delegate(
				object sender,
				RpsBlast.GetRPSBLASTResultCompletedEventArgs args
			) {
				if ( args.Result == "Seems to be running" ) {
					// Debug.WriteLine( String.Format( "PollRpsBlast: Seems to be running" ) );

					if ( RpsBlastCounter >= 0 ) {
						RpsBlastCounter--;
						PollRpsBlast( cleanup );
					}
					else {
						// Debug.WriteLine( String.Format( "PollRpsBlast: Time out" ) );
						cleanup();
						Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
							Alert.Show( "Time out in conserved domain blast:\n{0}\n", args.Result, args.Error );
						} ) );
					}
				}
				else if ( args.Error != null || args.Result.StartsWith( "Error" ) ) {
					// Debug.WriteLine( String.Format( "PollRpsBlast: Error out" ) );
					cleanup();
					Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
						Alert.Show( "Error in conserved domain blast:\n{0}\n", args.Result, args.Error );
					} ) );
				}
				else {
					//Debug.WriteLine( String.Format( "PollRpsBlast: received data I think." ) );
					//Debug.WriteLine( args.Result );

					try {
						BlastXmlParser parser = new BlastXmlParser();
						IList<BlastResult> results = parser.Parse( new StringReader( args.Result ) );
						cleanup();
						Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
							dataProducer.Load( results, dataProducer.SignalCollectionModified, SequenceSource.Genbank, SequenceSource.Cdd );
						} ) );
					}
					catch ( Exception e ) {
						cleanup();
						Application.Current.Dispatcher.BeginInvoke( new Action( delegate() {
							Debug.WriteLine( string.Format( "Exception in PollRpsBlast:\n{0}", e ) );
							statusBar.WriteLine( "Error processing conserved domain results" );
						} ) );
					}
				}
			};

			rpsBlast.GetRPSBLASTResultAsync( requestId.ToString() );
		}
		#endregion

		/// <summary>
		/// Blasts the reference sequence against the other sequences in the collection.
		/// <para>This adds new hits to the collection held by SilverMap, filling in the mutual distance matrix incrementally.</para>
		/// </summary>
		/// <param name="onSuccess">A callback that is invoked on successful completion of Bl2Seq.</param>

		private void BlastReferenceSequence ( Action onSuccess ) {
			// TODO: Fix things up when issue with multiple genes represented by single sequence has been resolved.
			Sequence referenceSequence = (Sequence) map.Model.ReferenceSequence;
			
			if ( referenceSequence.ContainsQuerySequence ) {
				SequenceKey queryKey = referenceSequence.EmbeddedQuerySequences.First();
				try {
					weCausedThisUpdate = true;
					map.Model.ReferenceSequence = dataProducer.model.sequences[queryKey];
					weCausedThisUpdate = false;
					return;
				}
				catch {}
			}

			if ( dataProducer.FeatureCollection.Focii.Contains( map.Model.ReferenceSequence ) ) {
				//
				return;
			}

			StringWriter subjects = new StringWriter();

			foreach ( var x in dataProducer.FeatureCollection.Domains ) {
				Sequence seq = (Sequence) x;
				string id = seq.Id;

				if ( seq != map.Model.ReferenceSequence && seq.Source == SequenceSource.Genbank ) {
					subjects.WriteLine( seq.Id );
				}
			}

			dataProducer.Bl2Seq(
				referenceSequence,
				subjects.ToString(),
				"blastp",
				"protein",
				eValueCutoff,
				hitListSize,
				Dispatcher,
				useBrowserProxy,
				onSuccess
			);

			if ( referenceSequence.SequenceData != null ) {
				DoRpsBlast( referenceSequence );
			}
			else if ( referenceSequence.Source == SequenceSource.Genbank ) {
				( new EUtil() ).EntrezQuery(
					EUtil.Database.protein,
					new string[] { referenceSequence.Id },
					DoRpsBlast,
					EntrezQueryFailed
				);
			}
			else {
				statusBar.WriteLine( "Conserved domains are not available for this query sequence." );
			}
		}

		private void NewFromGenomeDB ( object sender, RoutedEventArgs e ) {
			GenomeDBLoader loader = new GenomeDBLoader();
			loader.Initialise( statusBar, eValueCutoff, cddEValueCutoff, hitListSize, useBrowserProxy, selectedTaxa );

			if ( loader.ShowDialog() == true ) {
				useBrowserProxy = loader.UseBrowserProxy;
				eValueCutoff = loader.EValueCutoff;
				cddEValueCutoff = loader.CddEValueCutoff;
				hitListSize = loader.HitListSize;
				selectedTaxa = loader.SelectedTaxa;

				Blast(
					new String[] { GenomeDBLoader.QueryGI.ToString() },
					selectedTaxa,
					SequenceSource.Genbank
				);
			}
		}

		private void NewFromAccessionGI ( object sender, RoutedEventArgs e ) {
			// TODO: restore multiple gi capability when the issue related to multiple genes embedded in single hit is resolved.

			SimpleAccessionLoader loader = new SimpleAccessionLoader();
			loader.Initialise( eValueCutoff, cddEValueCutoff, hitListSize, useBrowserProxy, selectedTaxa );

			if ( loader.ShowDialog() == true ) {
				useBrowserProxy = loader.UseBrowserProxy == true;
				eValueCutoff = loader.EValueCutoff;
				cddEValueCutoff = loader.CddEValueCutoff;
				hitListSize = loader.HitListSize;
				selectedTaxa = loader.SelectedTaxa;

                string[] ids = SimpleAccessionLoader.Query.Split(new char[] { ',', ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
                string[] truncatedIds = new string[] { ids[0] };

                Blast(
                    truncatedIds,
                    loader.SelectedTaxa,
                    SequenceSource.Genbank
                );

			}

		}
        private void NewFromSequenceData(object sender, RoutedEventArgs e) {
            SequenceLoader loader = new SequenceLoader();
            loader.Initialise(eValueCutoff, cddEValueCutoff, hitListSize, useBrowserProxy, selectedTaxa);

            if (loader.ShowDialog() == true) {
                useBrowserProxy = loader.UseBrowserProxy == true;
                eValueCutoff = loader.EValueCutoff;
                cddEValueCutoff = loader.CddEValueCutoff;
                hitListSize = loader.HitListSize;

                Blast(
                    new string[] { SequenceLoader.Query }, 
                    null, 
                    SequenceSource.Unknown
                );
            }

        }

        private void MenuItem_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
        {
        	MenuItem item = sender as MenuItem;
			//item.Background = "#7F8599CE";
        }
	}
}
